interface IDataStructure {
    id?: number;
}


enum IndexedDBStatus {
    Success,
    Failed,
    Blocked,
    None
}

type IDBName = string

class IndexedDBService<T extends IDataStructure> {
    private static dbCache: { [key: IDBName]: IDBDatabase } = {}
    private dbName: string;
    private storeName: string;

    constructor(dbName: string, storeName: string) {
        this.dbName = dbName;
        this.storeName = storeName;
    }

    // override set and get
    get db() {
        const database = IndexedDBService.dbCache[this.dbName]
        if (!database) {
            console.error(`database(${this.dbName}) is null`)
        }
        return database
    }

    set db(value: IDBDatabase | null) {
        if (!this.dbName) {
            return
        }
        if (value) {
            IndexedDBService.dbCache[this.dbName] = value
            return
        }
        delete IndexedDBService.dbCache[this.dbName]
    }

    async createDB(dbName: string, storeName: string, version?: number): Promise<IndexedDBStatus> {
        return new Promise((resolve) => {
            const request = window.indexedDB.open(dbName, version);
            request.onerror = () => {
                resolve(IndexedDBStatus.Failed)
            };

            request.onsuccess = () => {
                this.db = request.result;
                IndexedDBService.dbCache[dbName] = this.db
                resolve(IndexedDBStatus.Success)
            };

            request.onblocked = () => {
                resolve(IndexedDBStatus.Blocked)
            };

            request.onupgradeneeded = (event) => {
                const db = (event.target as IDBOpenDBRequest).result;
                if (!db.objectStoreNames.contains(storeName)) {
                    db.createObjectStore(storeName, { autoIncrement: true });
                }
            };
        })
    }

    async init(): Promise<IndexedDBStatus> {
        const db = IndexedDBService.dbCache[this.dbName]
        if (db) {
            if (!db.objectStoreNames.contains(this.storeName)) {
                this.db = db
                return this.upgradeDatabase(this.storeName, db.version + 1)
            }
        }
        return this.createDB(this.dbName, this.storeName)
    }

    async upgradeDatabase(newStoreName: string, newVersion: number): Promise<IndexedDBStatus> {
        if (this.db) {
            this.db.close();
            this.db = null
        }
        return this.createDB(this.dbName, newStoreName, newVersion)
    }

    async add(data: T): Promise<number> {
        if (!this.db) return -1;
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readwrite');
                const store = transaction.objectStore(this.storeName);
                const request = store.add(data);
                request.onerror = () => {
                    console.error('Failed to add data to IndexedDB')
                    resolve(-1)
                };
                request.onsuccess = () => {
                    resolve(request.result as number);
                };
            } catch (error) {
                console.error(error)
                resolve(-1)
            }
        });
    }

    async bulkAdd(dataList: T[]): Promise<IndexedDBStatus> {
        if (!this.db) return IndexedDBStatus.None;
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readwrite');
                const store = transaction.objectStore(this.storeName);
                const promises: Promise<void>[] = [];
                dataList.forEach(data => {
                    const request = store.add(data);
                    const promise = new Promise<void>((innerResolve, innerReject) => {
                        request.onerror = () => {
                            innerReject(new Error('Failed to add data to IndexedDB'));
                        };
                        request.onsuccess = () => {
                            innerResolve();
                        };
                    });
                    promises.push(promise);
                });
                Promise.all(promises)
                    .then(() => resolve(IndexedDBStatus.Success))
                    .catch((error) => {
                        console.log(error)
                        resolve(IndexedDBStatus.Failed)
                    });
            } catch (error) {
                console.error(error)
                resolve(IndexedDBStatus.Failed)
            }
        });
    }

    async delete(query: (keyof T) | { [K in keyof T]?: T[K] }): Promise<IndexedDBStatus> {
        if (!this.db) return IndexedDBStatus.None;
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readwrite');
                const store = transaction.objectStore(this.storeName);
                const request = store.delete(query as IDBValidKey);
                request.onerror = () => {
                    console.error('Failed to delete data from IndexedDB')
                    resolve(IndexedDBStatus.Failed)
                };
                request.onsuccess = () => {
                    resolve(IndexedDBStatus.Success);
                };
            } catch (error) {
                console.error(error)
                resolve(IndexedDBStatus.Failed);
            }
        });
    }

    async update(query: { [K in keyof T]?: T[K] }, newData: Partial<T>): Promise<IndexedDBStatus> {
        if (!this.db) return IndexedDBStatus.None;
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readwrite');
                const store = transaction.objectStore(this.storeName);
                const request = store.openCursor(query as unknown as IDBKeyRange);
                request.onerror = () => {
                    console.error('Failed to update data in IndexedDB')
                    resolve(IndexedDBStatus.Failed)
                };
                request.onsuccess = (event) => {
                    const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
                    if (cursor) {
                        const updatedData = { ...cursor.value, ...newData };
                        cursor.update(updatedData);
                        resolve(IndexedDBStatus.Success);
                    } else {
                        console.error('No matching record found in IndexedDB')
                        resolve(IndexedDBStatus.Failed)
                    }
                };
            } catch (error) {
                console.error(error)
                resolve(IndexedDBStatus.Failed)
            }
        });
    }

    async getByQuery(query: { [K in keyof T]?: T[K] }): Promise<{ data?: T, status: IndexedDBStatus }> {
        if (!this.db) return { status: IndexedDBStatus.None };
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readonly');
                const store = transaction.objectStore(this.storeName);
                const keys = Object.keys(query) as Array<keyof T>;
                const key = keys[0];
                const index = store.index(String(key));
                const keyValue = query[key];
                const request = index.get(keyValue as IDBValidKey);
                request.onerror = () => {
                    console.error('Failed to get data from IndexedDB')
                    resolve({ status: IndexedDBStatus.Failed })
                };
                request.onsuccess = () => {
                    resolve({ data: request.result, status: IndexedDBStatus.Success });
                };
            } catch (error) {
                console.error(error)
                resolve({ status: IndexedDBStatus.Failed })
            }
        });
    }

    async clear(): Promise<IndexedDBStatus> {
        if (!this.db) return IndexedDBStatus.None
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readwrite');
                const store = transaction.objectStore(this.storeName);
                const request = store.clear();
                request.onerror = () => {
                    console.error('Failed to clear data from IndexedDB')
                    resolve(IndexedDBStatus.Failed);
                };
                request.onsuccess = () => {
                    resolve(IndexedDBStatus.Success);
                };
            } catch (error) {
                console.error(error)
                resolve(IndexedDBStatus.Failed)
            }
        });
    }


    async getAll(): Promise<T[]> {
        if (!this.db) return []
        return new Promise((resolve) => {
            try {
                const transaction = this.db!.transaction([this.storeName], 'readonly');
                const store = transaction.objectStore(this.storeName);
                const request = store.getAll();
                request.onerror = () => {
                    console.error('Failed to get data from IndexedDB')
                    resolve([])
                };
                request.onsuccess = () => {
                    resolve(request.result);
                };
            } catch (error) {
                console.error(error)
                resolve([])
            }
        });
    }

    async close() {
        this.db?.close()
    }
}

export default IndexedDBService
export type {
    IDataStructure
}
